/*
 * main.c
 *
 *  Created on: 18-Feb-2010
 *      Author: nrqm
 *
 *  Code for a stroboscopic guitar tuner.  The idea for this project is from here:
 *  	http://das-labor.org/wiki/Stimmmopped/en
 *
 *  There are two LEDs that strobe at a target frequency.  They are held near a resonating guitar string.  If the string
 *  is vibrating at the target frequency, the LEDs will light up the string at the same point in its oscillation.  If the
 *  string is out of tune it will be lit at different parts of its oscillation and it will look like there's a line of
 *  light drifting back and forth due to the stroboscopic effect:
 *  	http://en.wikipedia.org/wiki/Stroboscopic_effect
 *
 *  The target frequency is selected using an SP6T rotary switch connected to PA5..0.  The switch has a 7th off position;
 *  when it is selected, the CPU powers down.
 *
 *  The CPU must be running at 1 MHz for this to work properly.
 *
 *  See http://nrqm.pbworks.com/Guitar-Tuner
 */

#include <stdlib.h>
#include <stdio.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/wdt.h>

// Periods for open-string frequencies, in microseconds.
// The periods are divided by two to double the strobe frequency.  This causes the tuner to project two lines on the
// string envelope instead of one line, which makes it easier to use.
#define E2 (12134/2)	// 82.407 Hz
#define A2 (9090/2)		// 110.00 Hz
#define D3 (6810/2)		// 146.83 Hz
#define G3 (5102/2)		// 196.00 Hz
#define B3 (4050/2)		// 246.94 Hz
#define E4 (3034/2)		// 329.63 Hz
#define OFF 0			// Turn off the LEDs and put the CPU into a low-power state.

// time to wait after switch change in ms
#define DEBOUNCE_DELAY 150

volatile uint16_t period = OFF;					// The currently-selected strobe period
volatile uint8_t led_state = 0;					// Binary flag for toggling between the two strobe LEDs
volatile uint8_t pcint_flag = 0;				// Indicates to the system loop that the PCINT ISR ran
volatile uint8_t timer_flag = 0;				// Indicates to the system loop that the OC1A ISR ran
volatile uint16_t strobe_start = 0;				// Holds the time that the current LED strobe started
volatile uint16_t debounce_start = 0;			// Holds the time that the current rotary switch debounce delay started
volatile uint16_t now = 0;						// Holds the number of milliseconds since system start (or rollover)

/**
 * Get the raw (no debounce) 6-bit rotary switch state.  The bit corresponding to the currently selected switch position
 * is 1, and all other bits are 0.  If no switch position is selected (the pole is between contacts or in the off position)
 * then the return value is 0.
 */
inline uint8_t sample_rotary_switch()
{
	uint8_t value = PINA;
	return ~value & 0x3F;		// pins are pulled up, so the physical signal needs to be inverted.
}

/**
 * Determine the period for the strobe pulse that is currently selected on the rotary switch.
 */
void update_period()
{
	uint8_t sw = sample_rotary_switch();
	switch (sw)
	{
	case 0x00:
		period = OFF;
		break;
	case 0x01:
		period = E2;
		break;
	case 0x02:
		period = A2;
		break;
	case 0x04:
		period = D3;
		break;
	case 0x08:
		period = G3;
		break;
	case 0x10:
		period = B3;
		break;
	case 0x20:
		period = E4;
		break;
	default:
		period = OFF;
		break;
	}
}

inline void sleep()
{
	// put the inputs into output mode so that the pull-ups aren't drawing any current and turn off one strobe LED.
	// PA0 needs to be an input so that the CPU will wake up when the rotary switch is turned to position 1.
	DDRA = 0xFE;
	PORTA = 0x01;
	// Make sure that the other strobe LED is off
	PORTB &= ~_BV(PB2);
	// Need to add a few cycles to make sure that the change to PORTA doesn't count as a PCINT.
	__asm__ __volatile__ ("nop" "\n\r" ::);
	__asm__ __volatile__ ("nop" "\n\r" ::);
	__asm__ __volatile__ ("nop" "\n\r" ::);
	__asm__ __volatile__ ("nop" "\n\r" ::);
	__asm__ __volatile__ ("nop" "\n\r" ::);
	__asm__ __volatile__ ("nop" "\n\r" ::);
	__asm__ __volatile__ ("nop" "\n\r" ::);
	__asm__ __volatile__ ("nop" "\n\r" ::);
	// Power down until a pin change interrupt happens on PA0.
	sleep_cpu();

	// re-enable the inputs for the rest of the rotary switch.
	DDRA = 0xC0;
	PORTA = 0x3F;
}

inline uint16_t millis()
{
	uint16_t retval = 0;
	uint8_t sreg = SREG;
	cli();
	retval = now;
	SREG = sreg;
	return retval;
}

int main()
{
	uint16_t last_period = OFF;
	cli();

	// disable the watchdog timer
	wdt_disable();

	set_sleep_mode(SLEEP_MODE_PWR_DOWN);	// configure the sleep instruction to enter power-down mode
	sleep_enable();							// this doesn't enter sleep mode, it just enables the instruction

	// PA7 is unused; PA6 is a strobe LED output; PA5..0 are rotary switch inputs.
	DDRA = _BV(PA7) | _BV(PA6);
	// PB0 and PB1 are connected to the crystal; PB2 is a strobe LED output; PB3 is /RESET, so it can't be an I/O pin.
	DDRB = _BV(PB2);

	// Enable pull-ups on the rotary switch inputs.
	PORTA = _BV(PA5) | _BV(PA4) | _BV(PA3) | _BV(PA2) | _BV(PA1) | _BV(PA0);

	// Shut down the 8-bit timer, USI (serial), and ADC modules to reduce active current consumption a little bit.
	PRR = _BV(PRTIM0) | _BV(PRUSI) | _BV(PRADC);

	// Enable the pin change 0 interrupt to handle changes on the rotary switch inputs
	GIMSK = _BV(PCIE0);
	PCMSK0 = _BV(PCINT5) | _BV(PCINT4) | _BV(PCINT3) | _BV(PCINT2) | _BV(PCINT1) | _BV(PCINT0);

	// Start the 16-bit timer
	// The timer is configured for normal mode with no output, running at 1 MHz (no clock scaling).
	TCCR1A = 0;
	TCCR1B = _BV(CS10);
	update_period();		// Read the current rotary switch position
	OCR1A = period;			// and set the output compare timer to trigger at the selected output frequency
	OCR1B = 1000;			// tick at 1 ms intervals
	TIMSK1 = _BV(OCIE1A) | _BV(OCIE1B);		// Enable the ISRs
	TCNT1 = 0;

	sei();

	// make sure that the switch change code runs at startup to select the correct period.
	pcint_flag = 1;
	debounce_start = millis() - DEBOUNCE_DELAY;

	// System loop
	for (;;)
	{
		// This branch is taken if the rotary switch changed position or bounced recently (see PCINT0_vect).
		if (pcint_flag)
		{
			if (millis() - debounce_start > DEBOUNCE_DELAY)
			{
				// If DEBOUNCE_DELAY ms have expired since the last PCINT assertion, read the current switch value and change
				// the strobe LED PWM period.
				pcint_flag = 0;
				last_period = period;
				update_period();
				if (period == OFF && (last_period == E2 || last_period == OFF))
				{
					// If the switch transitioned from position 1 to its off position, power down the device.
					// This will also be executed if the switch is set between positions 1 and 2 for longer than
					// the debounce period.  If that happens then the device will turn off until it returns to
					// the E2 position.  I'm not sure how to avoid that except by increasing the debounce period.
					sleep();
					// the PCINT vector should run upon wake-up.
				}
			}
		}
		// This branch is taken if one of the strobe LEDs is currently turned on (see TIM1_COMPA_vect).
		if (timer_flag)
		{
			if (TCNT1 - strobe_start > period / 4)
			{
				// turn off whatever strobe LED is on after a 25% duty cycle
				timer_flag = 0;
				PORTB &= ~_BV(PB2);
				PORTA &= ~_BV(PA6);
			}
		}
	}
	return 0;
}

// Detect changes in the rotary switch position.  The change is handled in the system loop.
ISR(PCINT0_vect)
{
	// This interrupt is fired when the switch breaks a contact and when it makes the next contact.
	// We actually want to process the interrupt when the switch makes a contact, but because the switch might
	// be bouncing the interrupt can't tell if it's making or breaking without waiting for a few milliseconds.
	// Every time the pin bounces this ISR will run and reset the debounce timer.
	pcint_flag = 1;
	debounce_start = millis();
}

// Initiate an alternating PWM cycle on the strobe LEDs.  The pulse is brought low in the system loop.
ISR(TIM1_COMPA_vect)
{
	OCR1A += period;
	if (period == OFF)
	{
		// if the rotary switch is off, don't activate the strobe LEDs.
		return;
	}
	if (led_state)
	{
		PORTB |= _BV(PB2);
	}
	else
	{
		PORTA |= _BV(PA6);
	}
	led_state = !led_state;		// light the other LED next time!
	timer_flag = 1;
	strobe_start = TCNT1;
}

// Count milliseconds since the system started.  The global variable "now" is incremented every 1000 microseconds.
ISR(TIM1_COMPB_vect)
{
	OCR1B += 1000;
	now++;
}
